-
-
Notifications
You must be signed in to change notification settings - Fork 200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GasFeeController #494
Add GasFeeController #494
Conversation
src/gas/GasFee.controller.ts
Outdated
async getGasFeeEstimatesAndStartPolling( | ||
pollToken: string, | ||
): Promise<GasFeeState | undefined> { | ||
let gasEstimates; | ||
if (this.pollTokens.size > 0) { | ||
gasEstimates = this.state; | ||
} else { | ||
gasEstimates = await this._fetchGasFeeEstimateData(); | ||
} | ||
|
||
this._startPolling(pollToken); | ||
|
||
return gasEstimates; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not expect start polling to return the gasEstimates
since those will be updated in the state and we are following a reactive paradigm, the values should always be read from the state as it is the source of truth.
I would expect though that start polling returns the pollToken
or a fresh pollToken
in case the argument is falsy, consider this code:
// "Mounting / Unmounting / changing pollToken" effect
useEffect(() => {
const token = getGasFeeEstimatesAndStartPolling(pollToken);
setPollToken(token);
return () => {
if (token) {
disconnectPoller(token);
}
}
}, [setPollToken, pollToken]);
this would work in the case pollToken
is set or not:
const [pollToken, setPollToken] = useState(null);
// or
const [pollToken, setPollToken] = useState(txHash);
In order to do so:
- start polling with an existing poll token is a no op
- start polling with a falsy poll token returns a new poll token
I'd encourage the use of this pattern because we minimize UI <-> Controllers polling issues.
a70c5f5
to
2eb5fd9
Compare
0029514
to
ff358b7
Compare
src/util.ts
Outdated
export function gweiDecToWEIBN(n: number | string) { | ||
const wholePart = Math.floor(Number(n)); | ||
const decimalPartMatch = Number(n) | ||
.toFixed(3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh. So digits after the third are omitted 🤔
This seems fine, given that WEI isn't really divisible. But this is probably worth documenting at least, so it doesn't come as a surprise (e.g. "rounds down to the nearest WEI".).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, except I realize now that a GWEI is 1,000,000,000 WEI (10^9), and not just 1000 WEI (10^3) as this logic would imply.
🤦
changing to .toFixed(9)
should fix that.
b2a0f11
to
50da3b7
Compare
src/gas/gas-util.ts
Outdated
export async function fetchLegacyGasPriceEstimates(): Promise<{ | ||
low: string; | ||
medium: string; | ||
high: string; | ||
}> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason to not use the LegacyGasPriceEstimate
interface?
import { LegacyGasPriceEstimate } from './GasFeeController';
//...
export async function fetchLegacyGasPriceEstimates(): Promise<LegacyGasPriceEstimate> {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, Resolved 53250a9
src/network/NetworkController.ts
Outdated
if (error) { | ||
reject(error); | ||
} else { | ||
const isEIP1559Compatible = typeof block.baseFee !== undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be a string 'undefined'
, so typeof block.baseFee !== 'undefined';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, from testing on Ropsten, I believe it should be block.baseFeePerGas
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 2bdcd7d and baseFeePerGas is what we used on extension's version of this code with success.
): Promise<EthGasPriceEstimate> { | ||
const gasPrice = await query(ethQuery, 'gasPrice'); | ||
return { | ||
gasPrice: weiHexToGweiDec(gasPrice).toString(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be failing: Invalid character in 0x5f48b0f7
. Is weiHexToGweiDec
expecting other format?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe without the 0x
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aabfa3a good catch, added a stripHexPrefix in the conversion method and added a test
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
return { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be slow, average, fast?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it, but I think the synergy afforded of having gasPriceEstimates.low/medium/high on two of the three valid gas estimate types is better, and the mapping of slow/avg/fast can happen on app side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks Brad!
Thanks for the reviews @andrepimenta and @wachunei -- resolved two of the three comments with code changes and responded to the third. Let me know what you think 🙏🏻 |
Working great now, thanks!! |
src/network/NetworkController.ts
Outdated
const { properties = {} } = this.state; | ||
|
||
if (!properties.isEIP1559Compatible) { | ||
if (!this.ethQuery || !this.ethQuery.sendAsync) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use optional chaining here, if our project supports it:
if (!this?.ethQuery?.sendAsync) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved :)
a8b4bfd
to
5d843cd
Compare
…pdated gweiDecToWEIBN util
5d843cd
to
8d04cc5
Compare
}); | ||
|
||
describe('weiHexToGweiDec', () => { | ||
it('should convert a whole number to WEI', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The descriptions of these it
blocks need to change to reflect that the function is converting to GWEI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
41cdace done here
Since |
}); | ||
}); | ||
|
||
describe('weiHexToGweiDec', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be personal preference, but I think these tests can be clearer in intent, and also less prone to error, if the hard coded inputs and hard coded outputs are explicitly provided, as opposed to be calculated by other functions.
This is how I would write these tests, feel free to copy and paste:
describe('weiHexToGweiDec', () => {
it('should convert a whole number to WEI', () => {
const testData = [
{
input: '3b9aca00',
expectedResult: 1,
},
{
input: '1ca35f0e00',
expectedResult: 123,
},
{
input: '178411b200',
expectedResult: 101,
},
{
input: '11f5021b400',
expectedResult: 1234,
},
];
testData.forEach(({ input, expectedResult }) => {
expect(
util.weiHexToGweiDec(input),
).toBe(expectedResult);
});
});
it('should convert a number with a decimal part to WEI', () => {
const testData = [
{
input: '4190ab00',
expectedResult: 1.1,
},
{
input: '1ca3f7a480',
expectedResult: 123.01,
},
{
input: '178420f440',
expectedResult: 101.001,
},
{
input: '11f71ed6fc0',
expectedResult: 1234.567,
},
];
testData.forEach(({ input, expectedResult }) => {
expect(
util.weiHexToGweiDec(input),
).toBe(expectedResult);
});
});
it('should convert a number < 1 to WEI', () => {
const testData = [
{
input: '5f5e100',
expectedResult: 0.1,
},
{
input: '989680',
expectedResult: 0.01,
},
{
input: 'f4240',
expectedResult: 0.001,
},
{
input: '21cbbbc0',
expectedResult: 0.567,
},
];
testData.forEach(({ input, expectedResult }) => {
expect(
util.weiHexToGweiDec(input),
).toBe(expectedResult);
});
});
it('should work with 0x prefixed values', () => {
expect(util.weiHexToGweiDec('0x5f48b0f7')).toBe('1.598599415');
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not going to comment about this, but jest has a test.each
function which is my personal preference, either as string literal table or array table.
Example:
test.each`
a | b | expected
${1} | ${1} | ${2}
${1} | ${2} | ${3}
${2} | ${1} | ${3}
`('returns $expected when $a is added $b', ({a, b, expected}) => {
expect(a + b).toBe(expected);
});
Example from mobile:
https://github.com/MetaMask/metamask-mobile/blob/978b73d88a5208a7d5d74415af980a2f3a59403d/app/components/Base/Keypad/createKeypadRule.test.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copy and pasted, just had to update the expectedResult to strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Co-authored-by: ricky <ricky.miller@gmail.com> Co-authored-by: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Co-authored-by: Brad Decker <git@braddecker.dev>
Co-authored-by: ricky <ricky.miller@gmail.com> Co-authored-by: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Co-authored-by: Brad Decker <git@braddecker.dev>
This controller handles all logic for fetching gas fee estimates. Currently supports the following estimate types:
FeeMarketEstimateType
- EIP-1559 style maxFeePerGas/maxPriorityFeePerGas estimates along with timing data for displaying to users.LegacyEstimateType
- Fast/average/slow type gasPrice estimates, retrieved from the MetaSwaps APIEthGasPriceEstimateType
- a single gasPrice estimate fetched from eth_gasPrice.NoEstimateType
- This is the state shape when there is no currently active estimate type.This controller polls for gas fee estimates at a frequency specified by the
interval
prop passed into the constructor's options. Polling only happens when there are active subscribers to gas fees. Subscribers are issued a polling token, which can be used to unsubscribe from the controller. When all tokens are removed the polling mechanism stops. This allows multiple views and/or components to indicate their requirement for data.